<script type="text/javascript" src="essentials.js"></script>
<script type="text/javascript" src="actions.js"></script>
<script type="text/javascript">
/* constants */
var LBUTTON = 0, MBUTTON = 1, RBUTTON = 2;
var LEFT = 'L', RIGHT = 'R', UP = 'U', DOWN = 'D';
var OPTION_URL = chrome.extension.getURL('options.html');

/* all living tabs, including recently closed ones */
var _tabs = {
	/* number of revivable tabs */
	SIZE: 10,
	/* the stack of revivable tabs' id */
	closed: [],
	/* all tabs, opened and revivables */
	all: {}
};

/* effective config */
var _cfg;

/* default config */
var _defcfg = {
	'trigger': RBUTTON,	// mouse button to trigger stroke
	'timeout': 1000,	// a stroke sequence expires after timeout (milliseconds)
	'minstep': 20,		// minimum moving distance of a stroke (in px)
	'trail': 'black',	// show trails when doing mouse strokes
	'suppress': true,	// suppress stroke actions when holding ALT
	'categories': {
		'navigation': {
			'to-page-top': [UP],
			'to-page-bottom': [DOWN],
			'scroll-up-one-page': [UP, RIGHT, DOWN],
			'scroll-down-one-page': [DOWN, RIGHT, UP],
			'history-back': [LEFT],
			'history-forward': [RIGHT],
			'previous-tab': [UP, LEFT],
			'next-tab': [UP, RIGHT],
			'upper-level-in-url': [],
			'increase-number-in-url': [],
			'decrease-number-in-url': []
		},

		'window': {
			'new-window': [UP, DOWN, RIGHT, UP],
			'close-window': [LEFT, DOWN, RIGHT],
			//'minimize-window': [DOWN, LEFT],
			'maximize-window': [RIGHT, UP]
		},

		'tab': {
			'new-tab': [RIGHT, DOWN],
			'close-tab': [LEFT, UP, RIGHT],
			'undo-close-tab': [DOWN, RIGHT],
			'detach-tab': [LEFT, UP],
			'duplicate-tab': [],
			'close-other-tabs': []
		},

		'link': {
			'open-link-in-new-window': [DOWN, RIGHT, UP, RIGHT],
			'open-link-in-new-background-tab': [DOWN, RIGHT, DOWN],
			'open-link-in-new-foreground-tab': [DOWN, RIGHT, DOWN, UP],
			'bookmark-this-link': []
		},

		'image': {
			//'save-image': [],
			'view-image': [UP, RIGHT, DOWN, UP]
		},

		'bookmark': {
			'bookmark-this-page': [DOWN, RIGHT, UP, LEFT],
			'remove-bookmark': [LEFT, DOWN, RIGHT, UP]
		},

		'utilities': {
			'reload': [UP, DOWN],
			'skip-cache-reload': [UP, DOWN, UP],
			'stop-page-loading': [UP, DOWN, UP, RIGHT],
			'text-zoom-in': [],
			'text-zoom-out': [],
			'text-zoom-reset': [],
			'search-selected-text': [LEFT, DOWN, RIGHT, UP, LEFT],
			'view-source': [LEFT, DOWN, RIGHT, DOWN, LEFT],
			'take-screenshot': [RIGHT, LEFT, RIGHT, LEFT]
		},

		'other': {
			'mouse-stroke-options': [DOWN, UP, RIGHT, DOWN]
		}
	}
};

/* compare stroke sequences */
var match = function(seq1, seq2) {
	if (seq1 === seq2 || !seq1 && !seq2)
		return true;
	if (!seq1 && seq2 || seq1 && !seq2 || seq1.length != seq2.length)
		return false;

	return seq1.join(' ') == seq2.join(' ');
}

/* execute the given script in the given tab */
var tab_exec = function(tab, arg) {
	chrome.tabs.sendRequest(tab.id, {
		req: 'local-eval',
		arg: arg
	});
}

/* translate older version configurations to the newest version */
var sync_config = function() {
	/* is object but not array */
	var iterable = function(o) {
		return !!o && o.constructor == {}.constructor;
	}

	/* mirror o2 to o1 */
	var mirror = function(o1, o2) {
		if (iterable(o1) && iterable(o2)) {
			for (k in o1) {
				o1[k] = mirror(o1[k], o2[k]);
			}
		} else if (undefined != o2 && !iterable(o1) && !iterable(o2)) {
			o1 = o2;
		}
		return o1;
	}

	_cfg = mirror(JSON.parse(JSON.stringify(_defcfg)), _cfg);

	/* flat stroke categories for easy referring */
	_cfg.strokes = {};
	for (k in _cfg.categories) {
		for (k2 in _cfg.categories[k]) {
			_cfg.strokes[k2] = _cfg.categories[k][k2];
		}
	}
}

/* load user config */
var load_config = function(update_tabs) {
	try {
		if (!(_cfg = JSON.parse(localStorage.getItem('config')))) {
			_cfg = _defcfg;
		}
	} catch (ex) {
		console.error(ex);
		_cfg = _defcfg;
	}

	sync_config();
	localStorage.setItem('config', JSON.stringify(_cfg));

	if (!update_tabs) return;

	/* update config in all tabs */
	for (id in _tabs.all) {
		chrome.tabs.sendRequest(_tabs.all[id].id, {
			req: 'update-config',
			arg: _cfg
		});
	}
}

/* routine to execute when a tab is updated */
chrome.tabs.onUpdated.addListener(function(id, info) {
	if ('complete' !== info.status) return;

	chrome.tabs.get(id, function(tab) {
		if (!tab.url.match(/^(chrome|view)/)) {
			/* record all opened regular tabs */
			_tabs.all[id] = tab;
			/* broadcast configurations */
			chrome.tabs.sendRequest(id, {
				req: 'update-config',
				arg: _cfg
			});
		}
	});
});

/* routine to execute when a tab is destroied */
chrome.tabs.onRemoved.addListener(function(id) {
	/* push to the stack of recently closed tabs */
	if (!!_tabs.all[id]) {
		_tabs.closed.push(id);
		while (_tabs.SIZE < _tabs.closed.length) {
			/* discard tabs closed earlier */
			delete _tabs.all[_tabs.closed.splice(0, 1)[0]];
		}
	}
});

/* listen to the content script */
chrome.extension.onConnect.addListener(function(port) {
	if ('stroke.html-stroke' != port.name) return;
	/* don't respond to option page */
	if (port.tab.url == OPTION_URL) return;

	port.onMessage.addListener(function(path) {
		var id, func, strokes = path.strokes,
			/* port.sender.tab seems to be a snapshot of the tab
			 * object when it was on loading, some properties
			 * (i.e., .title, .favIconUrl) are always unset,
			 * so we use our cache if possible */
			tab = _tabs.all[port.sender.tab.id] || port.sender.tab;

		/* accept strokes */
		for (id in _cfg.strokes) {
			if (match(strokes, _cfg.strokes[id])) {
				func = _actions[id];
				if (func.local) {
					/* exec locally in content script */
					tab_exec(tab, {
						id: id,
						tab: tab,
						path: path
					});
				} else {
					/* call directly */
					_actions[id].call(tab, tab, path);
				}
				break;
			}
		}
	});
});

/* listen to the config page */
chrome.extension.onConnect.addListener(function(port) {
	if ('stroke.html-config' != port.name) return;

	port.onMessage.addListener(function(command) {
		if ('reload' == command) {
			load_config(true);
		} else if ('pull-config' == command) {
			chrome.tabs.sendRequest(port.sender.tab.id, {
				req: 'update-config',
				arg: _cfg
			});
		}
	});
});

/* always refresh _defcfg */
localStorage.setItem('def_config', JSON.stringify(_defcfg));
/* get user config */
load_config();
</script>